package io.gitee.zhangbinhub.acp.boot.component

import io.gitee.zhangbinhub.acp.boot.exceptions.WebException
import io.gitee.zhangbinhub.acp.boot.log.LogAdapter
import io.gitee.zhangbinhub.acp.core.common.CommonTools
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ZeroCopyHttpOutputMessage
import org.springframework.http.server.reactive.ServerHttpRequest
import org.springframework.http.server.reactive.ServerHttpResponse
import reactor.core.publisher.Mono
import java.io.*
import java.net.URLEncoder

/**
 * 文件下载处理组件
 *
 * @since JDK 17
 */
class FileDownLoadHandle(private val logAdapter: LogAdapter) {

    @Throws(WebException::class)
    @JvmOverloads
    fun downLoadFile(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filePath: String,
        allowPathRegexList: List<String>? = null,
        isDelete: Boolean = false,
        deleteWaitTime: Long? = null
    ) {
        val path = filePath.replace("/", File.separator).replace("\\", File.separator)
        val file = File(path)
        if (validatePath(file, allowPathRegexList)) {
            val filename = file.name
            response.reset()
            response.addHeader(
                HttpHeaders.CONTENT_DISPOSITION,
                "attachment;filename=" + URLEncoder.encode(filename, CommonTools.getDefaultCharset())
            )
            response.contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE
            response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes")
            var fromPos: Long = 0
            val downloadSize = file.length()
            if (request.getHeader(HttpHeaders.RANGE) == null) {
                response.setContentLengthLong(downloadSize)
            } else {
                response.status = HttpStatus.PARTIAL_CONTENT.value()
                val range = getRange(downloadSize, request.getHeader(HttpHeaders.RANGE))
                response.addHeader(HttpHeaders.CONTENT_RANGE, "bytes ${range[0]}-${range[1]}/${downloadSize}")
                response.setContentLengthLong(range[2])
                fromPos = range[0]
            }
            var fis: RandomAccessFile? = null
            var toClient: OutputStream? = null
            try {
                toClient = BufferedOutputStream(response.outputStream)
                fis = RandomAccessFile(file, "r")
                if (fromPos > 0) {
                    fis.seek(fromPos)
                }
                val buffer = ByteArray(2048)
                var count = 0 // 当前写到客户端的大小
                var i = fis.read(buffer)
                while (i != -1) {
                    toClient.write(buffer, 0, i)
                    count += i
                    i = fis.read(buffer)
                }
                toClient.flush()
                logAdapter.debug("download file Success:$filename")
                if (isDelete) {
                    CommonTools.doDeleteFile(file, true, deleteWaitTime)
                }
            } catch (e: Exception) {
                logAdapter.error(e.message, e)
                throw WebException(e.message)
            } finally {
                fis?.close()
                toClient?.close()
            }

        } else {
            throw WebException("download file failed,the file path is not correct")
        }
    }

    @Throws(WebException::class)
    @JvmOverloads
    fun downLoadFile(
        request: ServerHttpRequest,
        response: ServerHttpResponse,
        filePath: String,
        allowPathRegexList: List<String>? = null,
        isDelete: Boolean = false,
        deleteWaitTime: Long? = null
    ): Mono<Void> {
        val path = filePath.replace("/", File.separator).replace("\\", File.separator)
        val file = File(path)
        if (validatePath(file, allowPathRegexList)) {
            val filename = file.name
            response.headers.set(
                HttpHeaders.CONTENT_DISPOSITION,
                "attachment;filename=" + URLEncoder.encode(filename, CommonTools.getDefaultCharset())
            )
            response.headers.contentType = MediaType.APPLICATION_OCTET_STREAM
            response.headers.set(HttpHeaders.ACCEPT_RANGES, "bytes")
            var fromPos: Long = 0
            val downloadSize = file.length()
            if (request.headers.range.isEmpty()) {
                response.headers.contentLength = downloadSize
            } else {
                response.statusCode = HttpStatus.PARTIAL_CONTENT
                val range = getRange(downloadSize, request.headers.getFirst(HttpHeaders.RANGE))
                response.headers.set(HttpHeaders.CONTENT_RANGE, "bytes ${range[0]}-${range[1]}/${downloadSize}")
                response.headers.contentLength = range[2]
                fromPos = range[0]
            }
            if (isDelete) {
                CommonTools.doDeleteFile(file, true, deleteWaitTime)
            }
            return (response as ZeroCopyHttpOutputMessage).writeWith(file, fromPos, downloadSize - fromPos)
        } else {
            throw WebException("download file failed,the file path is not correct")
        }
    }

    private fun validatePath(file: File, allowPathRegexList: List<String>?): Boolean {
        if (!file.exists()) {
            throw WebException("the file is not exists")
        }
        val filterRegex: MutableList<String> = mutableListOf()
        if (allowPathRegexList.isNullOrEmpty()) {
            filterRegex.addAll(
                mutableListOf(
                    CommonTools.getWebRootAbsPath() + "${File.separator}files${File.separator}tmp${File.separator}.*",
                    CommonTools.getWebRootAbsPath() + "${File.separator}files${File.separator}upload${File.separator}.*",
                    CommonTools.getWebRootAbsPath() + "${File.separator}files${File.separator}download${File.separator}.*"
                )
            )
        } else {
            filterRegex.addAll(allowPathRegexList)
        }
        return pathFilter(filterRegex, file.canonicalPath)
    }

    /**
     * 文件路径过滤
     *
     * @param filterRegex 路径
     * @param path        待匹配路径
     * @return true-允许下载 false-不允许下载
     */
    private fun pathFilter(filterRegex: List<String>, path: String): Boolean {
        path.apply {
            for (regex in filterRegex) {
                if (CommonTools.regexPattern(regex.replace("\\", "/"), this.replace("\\", "/"))) {
                    return true
                }
            }
        }
        return false
    }

    private fun getRange(totalLength: Long, headerRange: String?): List<Long> =
        if (!CommonTools.isNullStr(headerRange)) {
            var toPos: Long = 0
            val bytes = headerRange!!.replace("bytes=", "")
            val ary = bytes.split("-")
            val fromPos: Long = ary[0].toLong()
            if (ary.size == 2) {
                toPos = ary[1].toLong()
            }
            val size = if (toPos > fromPos) {
                toPos - fromPos + 1
            } else {
                toPos = totalLength - 1
                totalLength - fromPos
            }
            listOf(fromPos, toPos, size)
        } else {
            listOf()
        }
}
